﻿using System.Collections.Generic;
using System.Security.AccessControl;
using System.Linq;

namespace System.IO
{
    public class InMemoryFileSystemEntryProperties
    {
        public FileAttributes Attributes { get; set; }
        public string FullPath { get; set; }
        public bool Exists { get; set; }
        public DateTime CreationTimeUtc { get; set; }
        public DateTime LastAccessTimeUtc { get; set; }
        public DateTime LastWriteTimeUtc { get; set; }
        // for files the size of file. for directories 0
        public long Length { get; set; }

        public void Assign(InMemoryFileSystemEntryProperties from)
        {
            Attributes = from.Attributes;
            FullPath = from.FullPath;
            Exists = from.Exists;
            CreationTimeUtc = from.CreationTimeUtc;
            LastAccessTimeUtc = from.LastAccessTimeUtc;
            LastWriteTimeUtc = from.LastWriteTimeUtc;
            Length = from.Length;
        }

        public InMemoryFileSystemEntryProperties Clone()
        {
            var properties = new InMemoryFileSystemEntryProperties();
            properties.Assign(this);
            return properties;
        }

        public bool IsDirectory { get { return GetAttribute(FileAttributes.Directory); } }

        public bool GetAttribute(FileAttributes attribute)
        {
            return (Attributes & attribute) == attribute;
        }

        public void SetAttribute(FileAttributes attribute, bool value)
        {
            if (value)
                Attributes = Attributes | attribute;
            else
                Attributes = Attributes & ~attribute;
        }
    }

    public sealed class InMemoryFileSystemStore
    {
        private class Entry
        {
            public string Name { get; set; }
            public InMemoryFileSystemEntryProperties Properties { get; set; }            
        }

        private sealed class FileEntry : Entry
        {

            public FileEntry(DirectoryEntry entry)
            {
                Properties = new InMemoryFileSystemEntryProperties();
                Content = new MemoryStream();
                Parent = entry;
            }

            public Stream Content { get; private set; }
            public DirectoryEntry Parent { get; private set; }
        }

        private sealed class DirectoryEntry : Entry
        {

            public DirectoryEntry(string fullpath, string name)
            {
                Name = name;
                
                Properties = new InMemoryFileSystemEntryProperties();
                Properties.Attributes = FileAttributes.Directory;
                Properties.Exists = true;
                Properties.FullPath = fullpath;
                Files = new List<FileEntry>();
                Directories = new List<DirectoryEntry>();
            }

            public DirectoryEntry Parent { get; set; }

            public List<FileEntry> Files { get; set; }
            public List<DirectoryEntry> Directories { get; set; }
        }

        public InMemoryFileSystemStore(bool empty = false)
        {
            _roots = new List<DirectoryEntry>();
            if (!empty) AddRoots(new[] {@"C:\"});
        }

        public InMemoryFileSystemStore(params string[] roots)
        {
            _roots = new List<DirectoryEntry>();
            AddRoots(roots);
        }

        private void AddRoots(IEnumerable<string> roots)
        {
            foreach (var root in roots)
            {
                var trimmedRoot = LongPathTools.Instance.GetPathComponents(root)[0];
                var entry = new DirectoryEntry(trimmedRoot, trimmedRoot);
                entry.Properties.Attributes = FileAttributes.Hidden | FileAttributes.System
                    | FileAttributes.Directory | FileAttributes.Archive;
                entry.Properties.CreationTimeUtc = DateTime.UtcNow;
                entry.Properties.LastWriteTimeUtc = DateTime.UtcNow;
                _roots.Add(entry);
            }
        }


        // root may be a drive, like C:\, or a network folder like, \\pc1\porn-video
        private readonly List<DirectoryEntry> _roots;

        public void CheckInternalDirectoriesConsistency()
        {
            var queue = new Queue<DirectoryEntry>();
            foreach (var root in _roots)
            {
                queue.Enqueue(root);
            }

            while (queue.Count != 0)
            {
                var directoryEntry = queue.Dequeue();
                foreach (var directory in directoryEntry.Directories)
                {
                    if (directory.Parent != directoryEntry) throw new Exception();
                    queue.Enqueue(directory);
                }
            }
        }

        public void Delete(string path)
        {
            LongPathTools.ThrowIfPathEmptyOrHasInvalidCharacters(path);

            // TODO: Check if there are any open streams in this directory/file.

            var entryInfo = FindEntry(path);
            if (entryInfo == null) throw new DirectoryNotFoundException();
            if ((entryInfo.Properties.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
                throw new IOException("Path is read-only.");
            var directory = entryInfo as DirectoryEntry;
            if (directory != null)
            {
                if (directory.Parent == null) throw new IOException("Cannot remove root folder.");
                if (directory.Files.Count > 0 || directory.Directories.Count > 0) throw new IOException("Directory is not empty.");
                directory.Parent.Directories.Remove(directory);
            }
            else
            {
                var file = (FileEntry)entryInfo;
                file.Parent.Files.Remove(file);
            }
        }

        public InMemoryFileInfo CopyFile(string sourceFilename, string destFileName, bool overwrite)
        {
            LongPathTools.ThrowIfPathEmptyOrHasInvalidCharacters(sourceFilename);
            LongPathTools.ThrowIfPathEmptyOrHasInvalidCharacters(destFileName);

            var srcEntry = FindEntry(sourceFilename);
            if (srcEntry == null) throw new FileNotFoundException(sourceFilename);
            if (srcEntry is DirectoryEntry) throw new ArgumentException("sourceFilename");

            var destDirectoryEntry = FindEntry(LongPathTools.Instance.GetDirectoryName(destFileName));
            if (destDirectoryEntry == null || destDirectoryEntry is FileEntry)
                throw new DirectoryNotFoundException();

            var destEntry = FindEntry(destFileName);
            if (destEntry != null && !overwrite) throw new IOException("Destination file exists.");
            
            if (destEntry != null) Delete(destFileName);
            using (var srcStream = OpenFile(sourceFilename, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (var destStream = OpenFile(destFileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
            {
                srcStream.CopyTo(destStream);
            }
            return GetFileInfo(destFileName);
        }

        public void MoveFileTo(string sourceFilename, string destFileName)
        {
            LongPathTools.ThrowIfPathEmptyOrHasInvalidCharacters(sourceFilename);
            LongPathTools.ThrowIfPathEmptyOrHasInvalidCharacters(destFileName);

            var srcEntry = FindEntry(sourceFilename);
            if (srcEntry == null) throw new FileNotFoundException(sourceFilename);
            if (srcEntry is DirectoryEntry) throw new ArgumentException("sourceFilename");

            var destDirectoryEntry = FindEntry(LongPathTools.Instance.GetDirectoryName(destFileName));
            if (destDirectoryEntry == null || destDirectoryEntry is FileEntry) throw new DirectoryNotFoundException();

            var destEntry = FindEntry(destFileName);
            if (destEntry != null) throw new IOException("Destination file exists.");

            using (var srcStream = OpenFile(sourceFilename, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (var destStream = OpenFile(destFileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
            {
                srcStream.CopyTo(destStream);
            }
            Delete(sourceFilename);
        }

        public IFileInfo Replace(string existingFile, string destinationFileName, string destinationBackupFileName, bool ignoreMetadataErrors)
        {
            throw new NotImplementedException();
        }

        public IDirectoryInfo GetDirectoryInfo(string path)
        {
            LongPathTools.ThrowIfPathEmptyOrHasInvalidCharacters(path);
            var entry = FindEntry(path);
            if (entry == null)
                return new InMemoryDirectoryInfo(this, path);
            return new InMemoryDirectoryInfo(this, entry.Properties.Clone());
        }

        public IDirectoryInfo CreateDirectory(string path)
        {
            LongPathTools.ThrowIfPathEmptyOrHasInvalidCharacters(path);

            var components = LongPathTools.Instance.GetPathComponents(path);

            var root = _roots.FirstOrDefault(t => t.Properties.FullPath == components.First());
            if (root == null) throw new DirectoryNotFoundException();

            var directory = root;
            var currentFullPath = root.Name;
            foreach (var component in components.Skip(1))
            {
                currentFullPath = LongPathTools.Instance.Combine(currentFullPath, component);

                var entry = FindEntry(path);
                if (entry is FileEntry) throw new IOException("File exists.");
                if (entry == null)
                {
                    var directoryEntry = new DirectoryEntry(currentFullPath, component) {Parent = directory};
                    entry = directoryEntry;
                    directory.Directories.Add(directoryEntry);
                }
                directory = (DirectoryEntry)entry;
            }

            return GetDirectoryInfo(path);
        }

        private IEnumerable<DirectoryEntry> TraverseDirectories(DirectoryEntry root)
        {
            var stack = new Stack<DirectoryEntry>();
            stack.Push(root);
            while (stack.Count > 0)
            {
                var dir = stack.Pop();
                foreach (var subDir in dir.Directories)
                {
                    yield return subDir;
                    stack.Push(subDir);
                }
            }
        }

        public IEnumerable<InMemoryDirectoryInfo> GetSubdirectories(string path, SearchOption searchOption)
        {
            var entry = FindEntry(path);
            if (entry == null || !(entry is DirectoryEntry)) throw new DirectoryNotFoundException();
            var directoryEntry = (DirectoryEntry)entry;

            IEnumerable<DirectoryEntry> result;

            if (searchOption == SearchOption.TopDirectoryOnly)
            {
                result = new List<DirectoryEntry>(new[] { directoryEntry });
            }
            else
            {
                result = TraverseDirectories(directoryEntry);
            }

            foreach (var traverseDirectory in result)
                foreach (var directory in traverseDirectory.Files)
                    yield return new InMemoryDirectoryInfo(this, directory.Properties.Clone());
        }

        public IEnumerable<InMemoryFileSystemEntryInfo> GetFiles(string path, SearchOption searchOption)
        {
            var entry = FindEntry(path);
            if (entry == null || !(entry is DirectoryEntry)) throw new DirectoryNotFoundException();
            var directoryEntry = (DirectoryEntry)entry;

            IEnumerable<DirectoryEntry> result;

            if (searchOption == SearchOption.TopDirectoryOnly)
            {
                result = new List<DirectoryEntry>(new[] {directoryEntry});
            }
            else
            {
                result = TraverseDirectories(directoryEntry);
            }

            foreach (var traverseDirectory in result)
                foreach (var file in traverseDirectory.Files)
                    yield return new InMemoryFileInfo(this, file.Properties.Clone());  
        }

        public IEnumerable<IFileSystemInfo> GetFileSystemEntries(string path, SearchOption searchOption)
        {
            return GetSubdirectories(path, searchOption).Cast<IFileSystemInfo>().Concat(GetFiles(path, searchOption));
        }

        public string[] GetLogicalDrives()
        {
            return _roots.Select(t => t.Properties.FullPath).ToArray();
        }

        public void MoveDirectory(string sourceDirName, string destDirName)
        {
            throw new NotImplementedException();
        }

        public void DeleteDirectory(string path, bool recursive)
        {
            throw new NotImplementedException();
        }

        public Stream OpenFile(string path, FileMode mode, FileAccess access, FileShare share)
        {
            throw new NotImplementedException();
        }

        public Stream CreateFile(string path, int bufferSize, FileOptions options, FileSecurity fileSecurity)
        {
            throw new NotImplementedException();
        }

        public InMemoryFileInfo GetFileInfo(string path)
        {
            var findEntry = FindEntry(path);
            if (findEntry == null)
                return new InMemoryFileInfo(this, path);
            return new InMemoryFileInfo(this, findEntry.Properties.Clone());
        }

        public void SetFileInfo(string path, InMemoryFileSystemEntryProperties properties)
        {
            var findEntry = FindEntry(path);
            if (findEntry == null) throw new FileNotFoundException();
            findEntry.Properties.Assign(properties);
        }

        public bool FileExists(string fullName)
        {
            var entry = FindEntry(fullName);
            return entry is FileEntry;
        }

        private Entry FindEntry(string path)
        {
            LongPathTools.ThrowIfPathEmptyOrHasInvalidCharacters(path);
            
            IEnumerable<Entry> current = _roots;

            var components = LongPathTools.Instance.GetPathComponents(path);

            for (int i = 0; i < components.Count; i++)
            {
                var entry = current.FirstOrDefault(t => t.Name.Equals(components[i], LongPathTools.PathComparison));
                var fileEntry = entry as FileEntry;
                var directoryEntry = entry as DirectoryEntry;
                if (fileEntry != null)
                {
                    return fileEntry;
                }
                if (directoryEntry != null)
                {
                    if (i != components.Count - 1)
                        current = directoryEntry.Directories;
                    else
                        return directoryEntry;
                }
                else return null;
            }
            return null;
        }
    }
}